package main

import (
	stdContext "context"
	"database/sql"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"os"
	"time"

	"github.com/bradfitz/gomemcache/memcache"
	_ "github.com/go-sql-driver/mysql"
	"github.com/gorilla/securecookie"
	"github.com/kataras/golog"
	"github.com/kataras/iris"
	"github.com/kataras/iris/context"
	"github.com/kataras/iris/sessions"
	"github.com/kataras/iris/sessions/sessiondb/redis"
	"github.com/kataras/iris/sessions/sessiondb/redis/service"
	mpoauth2 "gopkg.in/chanxuehong/wechat.v2/mp/oauth2"
	"gopkg.in/chanxuehong/wechat.v2/oauth2"
	openauth2 "gopkg.in/chanxuehong/wechat.v2/open/oauth2"
	yaml "gopkg.in/yaml.v2"
)

const (
	letterBytes   = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
	debug         = "true"
)

// 全局app对象
var app *iris.Application

// JandanAppConfig 全局配置对象
type JandanAppConfig struct {
	Database struct {
		User     string `yaml:"user,omitempty"`
		Password string `yaml:"password,omitempty"`
		Host     string `yaml:"host,omitempty"`
		Name     string `yaml:"name,omitempty"`
	} `yaml:"database,omitempty"`
	Memcached []string `yaml:"memcached,omitempty"`
	HTTP      struct {
		Listen string `yaml:"listen,omitempty"`
	} `yaml:"http,omitempty"`
	Wechat struct {
		ID       string `yaml:"id,omitempty"`
		Sec      string `yaml:"sec,omitempty"`
		Callback string `yaml:"callback,omitempty"`
	} `yaml:"wechat,omitempty"`
	Log struct {
		Level string `yaml:"level,omitempty"`
	} `yaml:"log,omitempty"`
	Cookie struct {
		SessionName string `yaml:"session_cookie_name,omitempty"`
		AuthName    string `yaml:"auth_cookie_name,omitempty"`
		Hash        string `yaml:"cookie_hash,omitempty"`
		Block       string `yaml:"cookie_block,omitempty"`
	} `yaml:"cookie,omitempty"`
}

// JandanApp 煎蛋APP对象
type JandanApp struct {
	Config       *JandanAppConfig
	SessionRedis *redis.Database
	WebApp       *iris.Application
	Logger       *golog.Logger
	Session      *sessions.Sessions
	Mysql        *sql.DB
	Memcached    *memcache.Client
	SecureCookie *securecookie.SecureCookie
}

func (ja *JandanApp) init() {
	var err error

	configFile, err := ioutil.ReadFile("./config.yaml")
	if err != nil {
		fmt.Printf("fail to read config file %s \n", err)
		os.Exit(1)
	}
	ja.Config = &JandanAppConfig{}
	err = yaml.Unmarshal(configFile, ja.Config)
	if err != nil {
		log.Fatalf("error: %v", err)
		os.Exit(1)
	}

	// ja.dbUser = os.Getenv("DB_USER")
	// ja.dbPassword = os.Getenv("DB_PASSWORD")
	// ja.dbHost = os.Getenv("DB_HOST")
	// ja.dbName = os.Getenv("DB_NAME")

	// ja.memcachedServers = os.Getenv("MEMCACHED_SERVERS")

	// ja.listen = os.Getenv("LISTEN")

	// ja.wxCallbackUri = os.Getenv("WX_CALLBACK_URI")
	// ja.wxAppId = os.Getenv("WX_APP_ID")
	// ja.wxAppSec = os.Getenv("WX_APP_SEC")

	// ja.logLevel = os.Getenv("LOG_LEVEL")

	// ja.sessionCookieName = os.Getenv("SESSION_COOKIE_NAME")
	// ja.authCookieName = os.Getenv("AUTH_COOKIE_NAME")
	// ja.cookieHash = os.Getenv("COOKIE_HASH")
	// ja.cookieBlock = os.Getenv("COOKIE_BLOCK")

	ja.SessionRedis = redis.New(service.Config{
		Network:     service.DefaultRedisNetwork,
		Addr:        service.DefaultRedisAddr,
		Password:    "",
		Database:    "",
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: service.DefaultRedisIdleTimeout,
		Prefix:      "session_",
	})

	ja.SessionRedis.Async(true)

	ja.WebApp = iris.New()

	ja.Logger = ja.WebApp.Logger()
	ja.Logger.SetOutput(os.Stdout)
	ja.Logger.SetLevel(ja.Config.Log.Level)

	view := iris.Django("./templates", ".html")
	ja.WebApp.RegisterView(view)

	ja.SecureCookie = securecookie.New([]byte(ja.Config.Cookie.Hash), []byte(ja.Config.Cookie.Block))
	// 初始化 session
	ja.Session = sessions.New(sessions.Config{
		Cookie: ja.Config.Cookie.SessionName,
		Encode: ja.SecureCookie.Encode,
		Decode: ja.SecureCookie.Decode,
	})

	ja.Session.UseDatabase(ja.SessionRedis)

	ja.Mysql, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4", ja.Config.Database.User, ja.Config.Database.Password, ja.Config.Database.Host, ja.Config.Database.Name))
	checkAndPanic(err)

	ja.Memcached = memcache.New(ja.Config.Memcached...)
	ja.Memcached.MaxIdleConns = 10

	iris.RegisterOnInterrupt(func() {
		ja.Mysql.Close()
		ja.SessionRedis.Close()
		timeout := 20 * time.Second
		ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout)
		defer cancel()
		ja.WebApp.Shutdown(ctx)
	})
}

func (ja *JandanApp) run() {

	ja.WebApp.OnErrorCode(iris.StatusInternalServerError, func(ctx context.Context) {
		ctx.HTML("Message: <b>" + ctx.Values().GetString("message") + "</b>")
	})

	ja.WebApp.Get("/", func(ctx context.Context) {
		s := ja.Session.Start(ctx)
		s.Set("a", 1)
		ja.Logger.Debug("hello world")
		ctx.HTML("<a href=\"./login\">微信登录点这里</a>")
	})

	ja.WebApp.Get("/error", func(ctx context.Context) {
		s := ja.Session.Start(ctx)
		v := s.Get("a")
		ja.Logger.Debug("session value %v", v)
		ja.checkAndShowErrorMsg(ctx, errors.New("GG"), "超载鸡出错啦！！！")
		return
	})

	ja.WebApp.Get("/login", func(ctx context.Context) {

		authCookie := ctx.GetCookie(ja.Config.Cookie.AuthName)
		ja.Logger.Debug("Login Cookie Encode Value: %v", authCookie)
		if authCookie != "" {
			cookieValue := make(map[string]string)
			var err error
			if err = ja.SecureCookie.Decode("jandan_auth", authCookie, &cookieValue); err == nil {
				ja.Logger.Debug("Login Cookie Decode Value: %v", cookieValue)
				ctx.Redirect("/")
				return
			}
		}

		state := randStringBytesMaskImprSrc(16)

		s := ja.Session.Start(ctx)

		s.Set("state", state)
		redirectURI := openauth2.AuthCodeURL(ja.Config.Wechat.ID, ja.Config.Wechat.Callback, "snsapi_login", state)
		ctx.Redirect(redirectURI, 302)
	})

	ja.WebApp.Get("/callback", func(ctx context.Context) {

		// 初始化session
		currentSession := ja.Session.Start(ctx)
		// 获取存储的state
		savedState := currentSession.GetString("state")

		code := ctx.URLParam("code")
		if code == "" {
			ja.showDenyPage(ctx, "用户禁止授权")
			return
		}

		queryState := ctx.URLParam("state")
		if queryState == "" {
			ja.showDenyPage(ctx, "状态参数为空")
			return
		}
		if savedState != queryState {
			ja.showDenyPage(ctx, "状态参数验证失败")
			return
		}

		oauth2Endpoint := mpoauth2.NewEndpoint(ja.Config.Wechat.ID, ja.Config.Wechat.Sec)

		oauth2Client := oauth2.Client{
			Endpoint: oauth2Endpoint,
		}
		token, err := oauth2Client.ExchangeToken(code)
		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10001") {
			return
		}

		stmt, err := ja.Mysql.Prepare("SELECT jmx.id, jm.login_status FROM jandan_members_weixin jmx INNER JOIN jandan_members jm ON jm.id = jmx.id WHERE jmx.unionid = ? ")
		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10002") {
			return
		}

		userData, err := stmt.Query(token.UnionId)
		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10002") {
			return
		}

		// 已注册用户
		if userData.Next() {
			var uid string
			var loginStatus string
			userData.Scan(&uid, &loginStatus)
			// 封禁判断
			if loginStatus == "1" {
				ja.showDenyPage(ctx, "此账号已经被封禁，无法登录")
				return
			}

			value := map[string]string{
				"uid": uid,
			}
			encoded, err := ja.SecureCookie.Encode(ja.Config.Cookie.AuthName, value)
			if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10003") {
				return
			}

			cookie := &http.Cookie{
				Name:     ja.Config.Cookie.AuthName,
				Value:    encoded,
				Path:     "/",
				Secure:   false,
				HttpOnly: false,
				Expires:  time.Now().Add(365 * 24 * time.Hour),
			}
			ctx.SetCookie(cookie)
			ctx.HTML("登录成功！")
			return
		}

		// 用户注册
		userInfo, err := mpoauth2.GetUserInfo(token.AccessToken, token.OpenId, "", nil)

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10004") {
			return
		}

		tMySQL, err := ja.Mysql.Begin()

		stmt, err = tMySQL.Prepare("INSERT INTO jandan_members VALUES (null, ?, ?, ?, ?)")
		now := time.Now()
		currentTime := fmt.Sprintf("%d-%02d-%02d %02d:%02d:%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
		result, err := stmt.Exec(
			userInfo.Nickname,
			currentTime,
			ctx.RemoteAddr(),
			1,
		)

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10005") {
			tMySQL.Rollback()
			return
		}

		userID, err := result.LastInsertId()

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10006") {
			tMySQL.Rollback()
			return
		}

		stmt, err = ja.Mysql.Prepare("INSERT INTO jandan_members_weixin VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10007") {
			tMySQL.Rollback()
			return
		}

		result, err = stmt.Exec(
			userID,
			userInfo.OpenId,
			userInfo.UnionId,
			userInfo.Sex,
			userInfo.City,
			userInfo.Province,
			userInfo.HeadImageURL,
			token.AccessToken,
			token.RefreshToken,
		)

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10007") {
			tMySQL.Rollback()
			return
		}

		err = tMySQL.Commit()

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10008") {
			tMySQL.Rollback()
			return
		}

		value := map[string]string{
			"uid": fmt.Sprintf("%d", userID),
		}

		encoded, err := ja.SecureCookie.Encode("jandan_auth", value)

		if ja.checkAndShowErrorMsg(ctx, err, "服务器错误：10009") {
			return
		}

		cookie := &http.Cookie{
			Name:     "jandan_auth",
			Value:    encoded,
			Path:     "/",
			Secure:   true,
			HttpOnly: false,
		}
		ctx.SetCookie(cookie)
		ctx.HTML("登录成功！！")

	})

	ja.WebApp.Run(iris.Addr(ja.Config.HTTP.Listen), iris.WithoutInterruptHandler)
}

func (ja *JandanApp) showDenyPage(ctx context.Context, msg string) {
	ctx.StatusCode(iris.StatusForbidden)
	ctx.ViewData("msg", msg)
	ctx.View("error.html")
}

func (ja *JandanApp) checkAndShowErrorMsg(ctx context.Context, err error, msg string) bool {
	if err != nil {
		logger := ja.WebApp.Logger()
		logger.Error("Exception: %v", err)
		ctx.StatusCode(iris.StatusInternalServerError)
		ctx.ViewLayout("layout.html")
		ctx.ViewData("msg", msg)
		ctx.View("error.html")
		return true
	}
	return false
}

func checkAndPanic(err error) {
	if err != nil {
		panic(err)
	}
}

func randStringBytesMaskImprSrc(n int) string {
	var src = rand.NewSource(time.Now().UnixNano())
	b := make([]byte, n)
	// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
	for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
		if remain == 0 {
			cache, remain = src.Int63(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}

	return string(b)
}

func main() {
	jandanApp := &JandanApp{}
	jandanApp.init()
	jandanApp.run()
}
